/*
 * Authors:
 *  Landon Fuller <landonf@plausiblelabs.com>
 *  Damian Morris <damian@moso.com.au>
 *
 * Copyright (c) 2008-2013 Plausible Labs Cooperative, Inc.
 * Copyright (c) 2010 MOSO Corporation, Pty Ltd.
 * All rights reserved.
 *
 * Permission is hereby granted, free of charge, to any person
 * obtaining a copy of this software and associated documentation
 * files (the "Software"), to deal in the Software without
 * restriction, including without limitation the rights to use,
 * copy, modify, merge, publish, distribute, sublicense, and/or sell
 * copies of the Software, and to permit persons to whom the
 * Software is furnished to do so, subject to the following
 * conditions:
 *
 * The above copyright notice and this permission notice shall be
 * included in all copies or substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
 * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
 * OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
 * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
 * HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
 * WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
 * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
 * OTHER DEALINGS IN THE SOFTWARE.
 */

#import "CrashReporter.h"

#import "PLCrashReportTextFormatter.h"
#import "PLCrashCompatConstants.h"
#import "PLCrashAsync.h"

@interface PLCrashReportTextFormatter (PrivateAPI)
static NSInteger binaryImageSort(id binary1, id binary2, void *context);
+ (NSString *) formatStackFrame: (PLCrashReportStackFrameInfo *) frameInfo
                     frameIndex: (NSUInteger) frameIndex
                         report: (PLCrashReport *) report
                           lp64: (BOOL) lp64;
@end


/**
 * Formats PLCrashReport data as human-readable text.
 */
@implementation PLCrashReportTextFormatter {

    /** Text output format. */
    PLCrashReportTextFormat _textFormat;

    /** Encoding to use for string output. */
    NSStringEncoding _stringEncoding;
}


/**
 * Formats the provided @a report as human-readable text in the given @a textFormat, and return
 * the formatted result as a string.
 *
 * @param report The report to format.
 * @param textFormat The text format to use.
 *
 * @return Returns the formatted result on success, or nil if an error occurs.
 */
+ (NSString *) stringValueForCrashReport: (PLCrashReport *) report withTextFormat: (PLCrashReportTextFormat) textFormat {
	NSMutableString* text = [NSMutableString string];
	boolean_t lp64 = true; // quiesce GCC uninitialized value warning

	/* Header */
	
    /* Map to apple style OS name */
    NSString *osName;
    switch (report.systemInfo.operatingSystem) {
        case PLCrashReportOperatingSystemMacOSX:
            osName = @"Mac OS X";
            break;
        case PLCrashReportOperatingSystemiPhoneOS:
            osName = @"iPhone OS";
            break;
        case PLCrashReportOperatingSystemiPhoneSimulator:
            osName = @"Mac OS X";
            break;
        case PLCrashReportOperatingSystemAppleTVOS:
            osName = @"Apple tvOS";
            break;
        default:
            osName = [NSString stringWithFormat: @"Unknown (%d)", report.systemInfo.operatingSystem];
            break;
    }
    
    /* Map to Apple-style code type, and mark whether architecture is LP64 (64-bit) */
    NSString *codeType = nil;
    {
        /* Attempt to derive the code type from the binary images */
        for (PLCrashReportBinaryImageInfo *image in report.images) {
            /* Skip images with no specified type */
            if (image.codeType == nil)
                continue;

            /* Skip unknown encodings */
            if (image.codeType.typeEncoding != PLCrashReportProcessorTypeEncodingMach)
                continue;
            
            switch (image.codeType.type) {
                case CPU_TYPE_ARM:
                    codeType = @"ARM";
                    lp64 = false;
                    break;
                    
                case CPU_TYPE_ARM64:
                    codeType = @"ARM-64";
                    lp64 = true;
                    break;

                case CPU_TYPE_X86:
                    codeType = @"X86";
                    lp64 = false;
                    break;

                case CPU_TYPE_X86_64:
                    codeType = @"X86-64";
                    lp64 = true;
                    break;

                case CPU_TYPE_POWERPC:
                    codeType = @"PPC";
                    lp64 = false;
                    break;
                    
                default:
                    // Do nothing, handled below.
                    break;
            }

            /* Stop immediately if code type was discovered */
            if (codeType != nil)
                break;
        }

        /* If we were unable to determine the code type, fall back on the processor info's value. */
        if (codeType == nil && report.systemInfo.processorInfo.typeEncoding == PLCrashReportProcessorTypeEncodingMach) {
            switch (report.systemInfo.processorInfo.type) {
                case CPU_TYPE_ARM:
                    codeType = @"ARM";
                    lp64 = false;
                    break;

                case CPU_TYPE_ARM64:
                    codeType = @"ARM-64";
                    lp64 = true;
                    break;

                case CPU_TYPE_X86:
                    codeType = @"X86";
                    lp64 = false;
                    break;

                case CPU_TYPE_X86_64:
                    codeType = @"X86-64";
                    lp64 = true;
                    break;

                case CPU_TYPE_POWERPC:
                    codeType = @"PPC";
                    lp64 = false;
                    break;

                default:
                    codeType = [NSString stringWithFormat: @"Unknown (%llu)", report.systemInfo.processorInfo.type];
                    lp64 = true;
                    break;
            }
        }
        
        /* If we still haven't determined the code type, we're totally clueless. */
        if (codeType == nil) {
            codeType = @"Unknown";
            lp64 = true;
        }
    }

    {
        NSString *hardwareModel = @"???";
        if (report.hasMachineInfo && report.machineInfo.modelName != nil)
            hardwareModel = report.machineInfo.modelName;

        NSString *incidentIdentifier = @"???";
        if (report.uuidRef != nil) {
            incidentIdentifier = (__bridge_transfer NSString *) CFUUIDCreateString(nil, report.uuidRef);
        }
    
        [text appendFormat: @"Incident Identifier: %@\n", incidentIdentifier];
        [text appendFormat: @"Hardware Model:      %@\n", hardwareModel];
    }
    
    /* Application and process info */
    {
        NSString *unknownString = @"???";
        
        NSString *processName = unknownString;
        NSString *processId = unknownString;
        NSString *processPath = unknownString;
        NSString *parentProcessName = unknownString;
        NSString *parentProcessId = unknownString;
        
        /* Process information was not available in earlier crash report versions */
        if (report.hasProcessInfo) {
            /* Process Name */
            if (report.processInfo.processName != nil)
                processName = report.processInfo.processName;
            
            /* PID */
            processId = [[NSNumber numberWithUnsignedInteger: report.processInfo.processID] stringValue];
            
            /* Process Path */
            if (report.processInfo.processPath != nil)
                processPath = report.processInfo.processPath;
            
            /* Parent Process Name */
            if (report.processInfo.parentProcessName != nil)
                parentProcessName = report.processInfo.parentProcessName;
            
            /* Parent Process ID */
            parentProcessId = [[NSNumber numberWithUnsignedInteger: report.processInfo.parentProcessID] stringValue];
        }
        
        NSString *versionString = report.applicationInfo.applicationVersion;
        /* Marketing version is optional */
        if (report.applicationInfo.applicationMarketingVersion != nil)
            versionString = [NSString stringWithFormat: @"%@ (%@)", report.applicationInfo.applicationMarketingVersion, report.applicationInfo.applicationVersion];
        
        [text appendFormat: @"Process:         %@ [%@]\n", processName, processId];
        [text appendFormat: @"Path:            %@\n", processPath];
        [text appendFormat: @"Identifier:      %@\n", report.applicationInfo.applicationIdentifier];
        [text appendFormat: @"Version:         %@\n", versionString];
        [text appendFormat: @"Code Type:       %@\n", codeType];
        [text appendFormat: @"Parent Process:  %@ [%@]\n", parentProcessName, parentProcessId];
    }
    
    [text appendString: @"\n"];
    
    /* System info */
    {
        NSString *osBuild = @"???";
        if (report.systemInfo.operatingSystemBuild != nil)
            osBuild = report.systemInfo.operatingSystemBuild;
        
        [text appendFormat: @"Date/Time:       %@\n", report.systemInfo.timestamp];
        [text appendFormat: @"OS Version:      %@ %@ (%@)\n", osName, report.systemInfo.operatingSystemVersion, osBuild];
        [text appendFormat: @"Report Version:  104\n"];        
    }

    [text appendString: @"\n"];

    /* Exception code */
    [text appendFormat: @"Exception Type:  %@\n", report.signalInfo.name];
    [text appendFormat: @"Exception Codes: %@ at 0x%" PRIx64 "\n", report.signalInfo.code, report.signalInfo.address];
    
    for (PLCrashReportThreadInfo *thread in report.threads) {
        if (thread.crashed) {
            [text appendFormat: @"Crashed Thread:  %ld\n", (long) thread.threadNumber];
            break;
        }
    }
    
    [text appendString: @"\n"];
    
    /* Uncaught Exception */
    if (report.hasExceptionInfo) {
        [text appendFormat: @"Application Specific Information:\n"];
        [text appendFormat: @"*** Terminating app due to uncaught exception '%@', reason: '%@'\n",
                report.exceptionInfo.exceptionName, report.exceptionInfo.exceptionReason];
        
        [text appendString: @"\n"];
    }

    /* If an exception stack trace is available, output an Apple-compatible backtrace. */
    if (report.exceptionInfo != nil && report.exceptionInfo.stackFrames != nil && [report.exceptionInfo.stackFrames count] > 0) {
        PLCrashReportExceptionInfo *exception = report.exceptionInfo;
        
        /* Create the header. */
        [text appendString: @"Last Exception Backtrace:\n"];

        /* Write out the frames. In raw reports, Apple writes this out as a simple list of PCs. In the minimally
         * post-processed report, Apple writes this out as full frame entries. We use the latter format. */
        for (NSUInteger frame_idx = 0; frame_idx < [exception.stackFrames count]; frame_idx++) {
            PLCrashReportStackFrameInfo *frameInfo = [exception.stackFrames objectAtIndex: frame_idx];
            [text appendString: [self formatStackFrame: frameInfo frameIndex: frame_idx report: report lp64: lp64]];
        }
        [text appendString: @"\n"];
    }

    /* Threads */
    PLCrashReportThreadInfo *crashed_thread = nil;
    NSInteger maxThreadNum = 0;
    for (PLCrashReportThreadInfo *thread in report.threads) {
        if (thread.crashed) {
            [text appendFormat: @"Thread %ld Crashed:\n", (long) thread.threadNumber];
            crashed_thread = thread;
        } else {
            [text appendFormat: @"Thread %ld:\n", (long) thread.threadNumber];
        }
        for (NSUInteger frame_idx = 0; frame_idx < [thread.stackFrames count]; frame_idx++) {
            PLCrashReportStackFrameInfo *frameInfo = [thread.stackFrames objectAtIndex: frame_idx];
            [text appendString: [self formatStackFrame: frameInfo frameIndex: frame_idx report: report lp64: lp64]];
        }
        [text appendString: @"\n"];

        /* Track the highest thread number */
        maxThreadNum = MAX(maxThreadNum, thread.threadNumber);
    }

    /* Registers */
    if (crashed_thread != nil) {
        [text appendFormat: @"Thread %ld crashed with %@ Thread State:\n", (long) crashed_thread.threadNumber, codeType];
        
        int regColumn = 0;
        for (PLCrashReportRegisterInfo *reg in crashed_thread.registers) {
            NSString *reg_fmt;
            
            /* Use 32-bit or 64-bit fixed width format for the register values */
            if (lp64)
                reg_fmt = @"%6s: 0x%016" PRIx64 " ";
            else
                reg_fmt = @"%6s: 0x%08" PRIx64 " ";
            
            /* Remap register names to match Apple's crash reports */
            NSString *regName = reg.registerName;
            if (report.machineInfo != nil && report.machineInfo.processorInfo.typeEncoding == PLCrashReportProcessorTypeEncodingMach) {
                PLCrashReportProcessorInfo *pinfo = report.machineInfo.processorInfo;
                cpu_type_t arch_type = pinfo.type & ~CPU_ARCH_MASK;

                /* Apple uses 'ip' rather than 'r12' on ARM */
                if (arch_type == CPU_TYPE_ARM && [regName isEqual: @"r12"]) {
                    regName = @"ip";
                }
            }
            [text appendFormat: reg_fmt, [regName UTF8String], reg.registerValue];

            regColumn++;
            if (regColumn == 4) {
                [text appendString: @"\n"];
                regColumn = 0;
            }
        }
        
        if (regColumn != 0)
            [text appendString: @"\n"];
        
        [text appendString: @"\n"];
    }
    
    /* Images. The iPhone crash report format sorts these in ascending order, by the base address */
    [text appendString: @"Binary Images:\n"];
    uint64_t lastImageBaseAddress = 0;
    for (PLCrashReportBinaryImageInfo *imageInfo in [report.images sortedArrayUsingFunction: binaryImageSort context: nil]) {
        /* Remove duplicates */
        uint64_t imageBaseAddress = [imageInfo imageBaseAddress];
        if (lastImageBaseAddress == imageBaseAddress) {
            continue;
        }
        lastImageBaseAddress = imageBaseAddress;

        NSString *uuid;
        /* Fetch the UUID if it exists */
        if (imageInfo.hasImageUUID)
            uuid = imageInfo.imageUUID;
        else
            uuid = @"???";
        
        /* Determine the architecture string */
        NSString *archName = @"???";
        if (imageInfo.codeType != nil && imageInfo.codeType.typeEncoding == PLCrashReportProcessorTypeEncodingMach) {
            switch (imageInfo.codeType.type) {
                case CPU_TYPE_ARM:
                    /* Apple includes subtype for ARM binaries. */
                    switch (imageInfo.codeType.subtype & ~CPU_SUBTYPE_MASK) {
                        case CPU_SUBTYPE_ARM_V6:
                            archName = @"armv6";
                            break;

                        case CPU_SUBTYPE_ARM_V7:
                            archName = @"armv7";
                            break;
                            
                        case CPU_SUBTYPE_ARM_V7S:
                            archName = @"armv7s";
                            break;

                        default:
                            archName = @"arm-unknown";
                            break;
                    }
                    break;
                    
                case CPU_TYPE_ARM64:
                    /* Apple includes subtype for ARM64 binaries. */
                    switch (imageInfo.codeType.subtype & ~CPU_SUBTYPE_MASK) {
                        case CPU_SUBTYPE_ARM64_ALL:
                            archName = @"arm64";
                            break;

                        case CPU_SUBTYPE_ARM64_V8:
                            archName = @"armv8";
                            break;

                        case CPU_SUBTYPE_ARM64E:
                            archName = @"arm64e";
                            break;

                        default:
                            archName = @"arm64-unknown";
                            break;
                    }
                    break;
                    
                case CPU_TYPE_X86:
                    archName = @"i386";
                    break;
                    
                case CPU_TYPE_X86_64:
                    archName = @"x86_64";
                    break;

                case CPU_TYPE_POWERPC:
                    archName = @"powerpc";
                    break;

                default:
                    // Use the default archName value (initialized above).
                    break;
            }
        }

        /* Determine if this is the main executable */
        NSString *binaryDesignator = @" ";
        if ([imageInfo.imageName isEqual: report.processInfo.processPath])
            binaryDesignator = @"+";
        
        /* base_address - terminating_address [designator]file_name arch <uuid> file_path */
        NSString *fmt = nil;
        if (lp64) {
            fmt = @"%18#" PRIx64 " - %18#" PRIx64 " %@%@ %@  <%@> %@\n";
        } else {
            fmt = @"%10#" PRIx64 " - %10#" PRIx64 " %@%@ %@  <%@> %@\n";
        }

        [text appendFormat: fmt,
                            imageInfo.imageBaseAddress,
                            imageInfo.imageBaseAddress + (MAX(1, imageInfo.imageSize) - 1), // The Apple format uses an inclusive range
                            binaryDesignator,
                            [imageInfo.imageName lastPathComponent],
                            archName,
                            uuid,
                            imageInfo.imageName];
    }
    

    return text;
}

/**
 * Initialize with the request string encoding and output format.
 *
 * @param textFormat Format to use for the generated text crash report.
 * @param stringEncoding Encoding to use when writing to the output stream.
 */
- (id) initWithTextFormat: (PLCrashReportTextFormat) textFormat stringEncoding: (NSStringEncoding) stringEncoding {
    if ((self = [super init]) == nil)
        return nil;
    
    _textFormat = textFormat;
    _stringEncoding = stringEncoding;

    return self;
}

// from PLCrashReportFormatter protocol
- (NSData *) formatReport: (PLCrashReport *) report error: (NSError **) outError {
    NSString *text = [PLCrashReportTextFormatter stringValueForCrashReport: report withTextFormat: _textFormat];
    return [text dataUsingEncoding: _stringEncoding allowLossyConversion: YES];
}
		 
@end


@implementation PLCrashReportTextFormatter (PrivateMethods)

/**
 * Format a stack frame for display in a thread backtrace.
 *
 * @param frameInfo The stack frame to format
 * @param frameIndex The frame's index
 * @param report The report from which this frame was acquired.
 * @param lp64 If YES, the report was generated by an LP64 system.
 *
 * @return Returns a formatted frame line.
 */
+ (NSString *) formatStackFrame: (PLCrashReportStackFrameInfo *) frameInfo
                     frameIndex: (NSUInteger) frameIndex
                         report: (PLCrashReport *) report
                           lp64: (BOOL) lp64
{
    /* Base image address containing instrumention pointer, offset of the IP from that base
     * address, and the associated image name */
    uint64_t baseAddress = 0x0;
    uint64_t pcOffset = 0x0;
    NSString *imageName = @"\?\?\?";
    NSString *symbolString = nil;

    PLCrashReportBinaryImageInfo *imageInfo = [report imageForAddress:frameInfo.instructionPointer];
    if (imageInfo != nil) {
        imageName = [imageInfo.imageName lastPathComponent];
        baseAddress = imageInfo.imageBaseAddress;
        pcOffset = frameInfo.instructionPointer - imageInfo.imageBaseAddress;
    } else if (frameInfo.instructionPointer) {
        PLCR_LOG("Cannot find image for 0x%" PRIx64, frameInfo.instructionPointer);
    }

    /* If symbol info is available, the format used in Apple's reports is Sym + OffsetFromSym. Otherwise,
     * the format used is imageBaseAddress + offsetToIP */
    if (frameInfo.symbolInfo != nil) {
        NSString *symbolName = frameInfo.symbolInfo.symbolName;

        /* Apple strips the _ symbol prefix in their reports. */
        if ([symbolName rangeOfString: @"_"].location == 0 && [symbolName length] > 1) {
            switch (report.systemInfo.operatingSystem) {
                case PLCrashReportOperatingSystemMacOSX:
                case PLCrashReportOperatingSystemiPhoneOS:
                case PLCrashReportOperatingSystemAppleTVOS:
                case PLCrashReportOperatingSystemiPhoneSimulator:
                    symbolName = [symbolName substringFromIndex: 1];
                    break;

                default:
                    PLCR_LOG("Symbol \"%s\" prefix rules are unknown for this OS!", [symbolName UTF8String]);
                    break;
            }
        }
        
        
        uint64_t symOffset = frameInfo.instructionPointer - frameInfo.symbolInfo.startAddress;
        symbolString = [NSString stringWithFormat: @"%@ + %" PRId64, symbolName, symOffset];
    } else {
        symbolString = [NSString stringWithFormat: @"0x%" PRIx64 " + %" PRId64, baseAddress, pcOffset];
    }

    /* Note that width specifiers are ignored for %@, but work for C strings.
     * UTF-8 is not correctly handled with %s (it depends on the system encoding), but
     * UTF-16 is supported via %S, so we use it here */
    return [NSString stringWithFormat: @"%-4ld%-35S 0x%0*" PRIx64 " %@\n",
            (long) frameIndex,
            (const uint16_t *)[imageName cStringUsingEncoding: NSUTF16StringEncoding],
            lp64 ? 16 : 8, frameInfo.instructionPointer,
            symbolString];
}

/**
 * Sort PLCrashReportBinaryImageInfo instances by their starting address.
 */
static NSInteger binaryImageSort(id binary1, id binary2, void *context) {
    uint64_t addr1 = [binary1 imageBaseAddress];
    uint64_t addr2 = [binary2 imageBaseAddress];
    
    if (addr1 < addr2)
        return NSOrderedAscending;
    else if (addr1 > addr2)
        return NSOrderedDescending;
    else
        return NSOrderedSame;
}

@end
